
local rc = {peripheral.find("raycaster")}
local gpu = peripheral.find("tm_gpu")
local properties, system
-- 配置参数
local euler_mode = false   
local cache = true 
local vector_fov = 1 

-- 颜色定义
local ENTITY_COLOR = {255, 0, 0}    -- 实体：红色
local SKY_COLOR = {173, 216, 230}     -- 天空：浅蓝色
local NO_DEVICE_COLOR = {100, 100, 150} -- 无设备时的灰蓝色
local ship_color = {255, 255, 0}      -- 飞船：黄色
local Projectile_color = {255, 0, 255}      -- 投射物：品红色
local SET_COLOR = {255, 165, 0} -- 目标颜色：橙色
local CENTER_COLOR = {0, 255, 0} -- 中心点颜色：绿色

-- 缓存配置
local CACHE_TIMEOUT = 1000  -- 缓存有效期1秒
local distance_cache = {}    -- 距离缓存表
-- 系统数据管理
system = {
    fileName = "cam_dat",
    file = nil,
}

system.init = function()
    system.file = io.open(system.fileName, "r")
    if system.file then
        local tmpProp = textutils.unserialise(system.file:read("a"))
        properties = system.reset()
        for k, v in pairs(properties) do
            if tmpProp[k] then
                properties[k] = tmpProp[k]
            end
        end
        system.file:close()
    else
        properties = system.reset()
        system.updatePersistentData()
    end
end

system.reset = function()
    return {
        number = 1,
        distance = 128,
        ppi = 32,
        angle = 45,
        invertdepth = false,
        set = "0v0"
    }
end
system.updatePersistentData = function()
    system.write(system.fileName, properties)
end

system.write = function(file, obj)
    system.file = io.open(file, "w")
    system.file:write(textutils.serialise(obj))
    system.file:close()
end

system.init()

local function check_rc_device()
    return rc and #rc > 0 and rc[properties.number] ~= nil
end

local width, height, img, width_roll_range, height_pitch_range, rcset, ray_directions

local function init_gpu_variables()
    gpu.refreshSize()
    gpu.fill()
    gpu.sync()
    gpu.setSize(properties.ppi)
    width, height = gpu.getSize()
    img = gpu.newImage(width, height)
    width_roll_range = math.rad(properties.angle)
    height_pitch_range = math.rad(properties.angle) * (height/width)
    if check_rc_device() then
        rcset = rc[properties.number]
    else
        rcset = nil
    end
end

init_gpu_variables()

local IGNORE_ENTITIES = {
    "arrow",
    "shell",
    "shot",
}

local function should_ignore_entity(item)
    if not item.is_entity or not item.descriptionId then
        return false
    end
    
    local description_lower = item.descriptionId:lower()
    for _, keyword in ipairs(IGNORE_ENTITIES) do
        if string.find(description_lower, keyword) then
            return true
        end
    end
    return false
end

local function linspace(start, end_, num)
    if num <= 1 then return {start} end
    local result = {}
    local delta = (end_ - start) / (num - 1)
    for i = 0, num - 1 do
        result[i + 1] = start + delta * i
    end
    return result
end

local function precompute_ray_directions()
    local x_axis = linspace(-width_roll_range, width_roll_range, width)
    local y_axis = linspace(-height_pitch_range, height_pitch_range, height)
    
    local directions = {}
    for y = 1, height do
        for x = 1, width do
            local index = (y-1) * width + x
            directions[index] = {
                vector = {y_axis[y], x_axis[x], vector_fov},
                screen_x = x - 1,
                screen_y = height - y,
                is_center = (x == math.floor(width/2) + 1 and y == math.floor(height/2) + 1)
            }
        end
    end
    return directions
end

ray_directions = precompute_ray_directions()

local function cached_raycast(direction_index, direction_vector)
    if not rcset then
        return { distance = properties.distance }
    end
    local cache_entry = distance_cache[direction_index]
    local current_time = os.epoch("utc")
    if cache_entry and (current_time - cache_entry.timestamp) < CACHE_TIMEOUT then
        return cache_entry.item
    end
    local item = rcset.raycast(properties.distance, direction_vector,euler_mode, true, cache)
    distance_cache[direction_index] = {
        item = item,
        timestamp = current_time
    }
    
    return item
end

local function cleanup_cache()
    local current_time = os.epoch("utc")
    for index, entry in pairs(distance_cache) do
        if (current_time - entry.timestamp) >= CACHE_TIMEOUT then
            distance_cache[index] = nil
        end
    end
end

local function get_pixel_color(item, is_center)
    if is_center then
        return CENTER_COLOR
    end
    
    if not rcset then
        return NO_DEVICE_COLOR
    end
    
    local distance = item.distance or properties.distance
    local normalized = (distance / properties.distance) * 255
    local intensity
    
    if properties.invertdepth then
        intensity = math.floor(normalized)  -- 反向：越远越亮
    else
        intensity = 255 - math.floor(normalized)  -- 正常：越近越亮
    end
    
    intensity = math.max(0, math.min(255, intensity))
    if item.descriptionId and string.find(item.descriptionId:lower() , properties.set) then
        return SET_COLOR
    elseif should_ignore_entity(item) then
        if properties.invertdepth == true then intensity = 255 - intensity end
        local r = math.floor(Projectile_color[1] * intensity / 255)
        local g = math.floor(Projectile_color[2] * intensity / 255)
        local b = math.floor(Projectile_color[3] * intensity / 255)
        return {r, g, b}
    elseif item.ship_id ~= nil then
        if properties.invertdepth == true then intensity = 255 - intensity end
        local r = math.floor(ship_color[1] * intensity / 255)
        local g = math.floor(ship_color[2] * intensity / 255)
        local b = math.floor(ship_color[3] * intensity / 255)
        return {r, g, b}
    elseif item.is_entity  then
        if properties.invertdepth == true then intensity = 255 - intensity end
        local r = math.floor(ENTITY_COLOR[1] * intensity / 255)
        local g = math.floor(ENTITY_COLOR[2] * intensity / 255)
        local b = math.floor(ENTITY_COLOR[3] * intensity / 255)
        return {r, g, b}
    elseif intensity == 0 or properties.invertdepth == true and intensity == 255 then
        return SKY_COLOR
    else
        return {intensity, intensity, intensity}
    end
end

local last_cleanup = os.epoch("utc")
local last_device_check = os.epoch("utc")
local DEVICE_CHECK_INTERVAL = 1000 -- 每1秒检查一次设备

local function yield()
    os.queueEvent("yield")
    os.pullEvent("yield")
end

local function render_no_device_screen()
    for y = 1, height do
        for x = 1, width do
            img.setRGB(x-2, y-2, NO_DEVICE_COLOR[1], NO_DEVICE_COLOR[2], NO_DEVICE_COLOR[3])
        end
    end
    gpu.drawImage(1, 1, img.ref())
    gpu.sync()
end

local cam = function ()
    while true do
        local current_time = os.epoch("utc")
        if current_time - last_device_check > DEVICE_CHECK_INTERVAL then
            local has_device = check_rc_device()
            if has_device and not rcset then
                init_gpu_variables()
                ray_directions = precompute_ray_directions()
                properties._needs_update = nil
            elseif not has_device and rcset then
                rcset = nil
            end
            last_device_check = current_time
        end
        if properties._needs_update then
            init_gpu_variables()
            ray_directions = precompute_ray_directions()
            properties._needs_update = nil
        end
        
        if not rcset then
            render_no_device_screen()
            sleep(1) -- 等待1秒再检查
        else
            for y = 1, height do
                for x = 1, width do
                    local index = (y-1) * width + x
                    local data = ray_directions[index]
                    local item
                    if data.is_center then
                        item = { distance = properties.distance }
                    else
                        item = cached_raycast(index, data.vector)
                    end
                    local r, g, b = table.unpack(get_pixel_color(item, data.is_center))
                    img.setRGB(data.screen_x-1, data.screen_y-1, r, g, b)
                end
                if y % 2 == 0 then
                    yield()
                end
            end
            gpu.drawImage(1, 1, img.ref())
            gpu.sync()
            -- 定期清理缓存（每1秒）
            if os.epoch("utc") - last_cleanup > 1000 then
                cleanup_cache()
                last_cleanup = os.epoch("utc")
            end
        end
    end
end

local termUtil = {
    cpX = 1,
    cpY = 1,
}

local absTextField = {
    x = 1,
    y = 1,
    len = 15,
    text = "",
    textCorlor = "0",
    backgroundColor = "8",
}

local genStr = function(s, count)
    local result = ""
    for i = 1, count, 1 do
        result = result .. s
    end
    return result
end

function absTextField:paint()
    local str = ""
    for i = 1, self.len, 1 do
        local text = tostring(self.key[self.value])
        local tmp = string.sub(text, i, i)
        if #tmp > 0 then
            str = str .. tmp
        else
            local tmp2 = ""
            for j = 0, self.len - i, 1 do
                tmp2 = tmp2 .. " "
            end
            str = str .. tmp2
            break
        end
    end

    term.setCursorPos(self.x, self.y)
    term.blit(str, genStr(self.textCorlor, #str), genStr(self.backgroundColor, #str))
end

function absTextField:inputChar(char)
    local xPos, yPos = term.getCursorPos()
    xPos = xPos + 1 - self.x
    local field = tostring(self.key[self.value])
    if #field < self.len then
        if self.type == "number" then
            if char >= '0' and char <= '9' then
                if field == "0" then
                    field = char
                else
                    field = string.sub(field, 1, xPos) .. char .. string.sub(field, xPos, #field)
                end

                self.key[self.value] = tonumber(field)
                termUtil.cpX = termUtil.cpX + 1
            end
            if char == '-' then
                self.key[self.value] = -self.key[self.value]
            end
        elseif self.type == "string" then
            local strEnd = string.sub(field, xPos, #field)
            field = string.sub(field, 1, xPos - 1) .. char .. strEnd
            self.key[self.value] = field
            termUtil.cpX = termUtil.cpX + 1
        end
    end
end

function absTextField:inputKey(key)
    local xPos, yPos = term.getCursorPos()
    local field = tostring(self.key[self.value])
    local minXp = self.x
    local maxXp = minXp + #field
    if key == 259 or key == 261 then 
        if xPos > minXp then
            termUtil.cpX = termUtil.cpX - 1
            if #field > 0 and termUtil.cpX > 1 then
                local index = termUtil.cpX - self.x
                field = string.sub(field, 1, index) .. string.sub(field, index + 2, #field)
            end
            if self.type == "number" then
                local number = tonumber(field)
                if not number then
                    self.key[self.value] = 0
                else
                    self.key[self.value] = number
                end
            elseif self.type == "string" then
                self.key[self.value] = field
            end
        end
    elseif key == 257 or key == 335 then
    elseif key == 262 or key == 263 then
        if key == 262 then
            termUtil.cpX = termUtil.cpX + 1
        elseif key == 263 then
            termUtil.cpX = termUtil.cpX - 1
        end
    elseif key == 264 or key == 258 then
    elseif key == 265 then
    end
    termUtil.cpX = termUtil.cpX > maxXp and maxXp or termUtil.cpX
    termUtil.cpX = termUtil.cpX < minXp and minXp or termUtil.cpX
end

function absTextField:click(x, y)
    local xPos = self.x
    if x >= xPos then
        if x < xPos + #tostring(self.key[self.value]) then
            termUtil.cpX, termUtil.cpY = x, y
        else
            termUtil.cpX, termUtil.cpY = xPos + #tostring(self.key[self.value]), y
        end
    end
end

local newTextField = function(key, value, x, y)
    return setmetatable({ key = key, value = value, type = type(key[value]), x = x, y = y },
        { __index = absTextField })
end

local absSelectBox = {
    x = 1,
    y = 1,
    label = "",
    contents = {},
    count = 0,
    interval = 0,
    fontColor = "8",
    backgroundColor = "f",
    selectColor = "e"
}

function absSelectBox:paint()
    term.setCursorPos(self.x, self.y)
    local select = tostring(self.key[self.value])
    for i = 1, #self.contents, 1 do
        local str = tostring(self.contents[i])
        if select == str then
            term.blit(str, genStr(self.backgroundColor, #str), genStr(self.selectColor, #str))
        else
            term.blit(str, genStr(self.fontColor, #str), genStr(self.backgroundColor, #str))
        end
        for j = 1, self.interval, 1 do
            term.write(" ")
        end
    end
end

function absSelectBox:click(x, y)
    local xPos = x + 1 - self.x
    local index = 0
    for i = 1, #self.contents, 1 do
        if xPos >= index and xPos <= index + #tostring(self.contents[i]) then
            self.key[self.value] = self.contents[i]
            properties._needs_update = true  -- 标记需要更新
            break
        end
        index = index + #tostring(self.contents[i]) + self.interval
    end
end

local newSelectBox = function(key, value, interval, x, y, ...)
    return setmetatable(
        { key = key, value = value, interval = interval, x = x, y = y, type = type(key[value]), contents = { ... } },
        { __index = absSelectBox })
end

-- 滑块对象
local absSlider = {
    x = 1,
    y = 1,
    min = 0,
    max = 1,
    len = 20,
    fontColor = "8",
    backgroundColor = "f"
}

function absSlider:paint()
    local field = self.key[self.value]
    if field == "-" then
        field = 0
    end
    local maxVal = self.max - self.min
    local xPos = math.floor((field - self.min) * (self.len / maxVal) + 0.5)
    xPos = xPos < 1 and 1 or xPos
    term.setCursorPos(self.x, self.y)
    for i = 1, self.len, 1 do
        if xPos == i then
            term.blit(" ", self.backgroundColor, self.fontColor)
        else
            term.blit("-", self.fontColor, self.backgroundColor)
        end
    end
end

function absSlider:click(x, y)
    local xPos = x + 1 - self.x
    if xPos > self.len then
        xPos = self.len
    end
    self.key[self.value] = math.floor((self.max - self.min) * (xPos / self.len) + 0.5) + self.min
    properties._needs_update = true  -- 标记需要更新
end

local newSlider = function(key, value, min, max, len, x, y)
    return setmetatable({ key = key, value = value, min = min, max = max, len = len, x = x, y = y },
        { __index = absSlider })
end

local runTerm = function()
    local fieldTb = {
        distance = newTextField(properties, "distance", 11, 5),
        ppi = newTextField(properties, "ppi", 6, 7),
        angle = newTextField(properties, "angle", 8, 9),
        set = newTextField(properties, "set", 6, 13),
    }
    fieldTb.distance.len = 3
    fieldTb.ppi.len = 3
    fieldTb.angle.len = 2
    fieldTb.set.len = 20
    local selectBoxTb = {
        number1 = newSelectBox(properties, "number", 3, 11, 11, 1, 2, 3, 4, 5, 6, 7, 8),
        number2 = newSelectBox(properties, "number", 3, 11, 12, 9, 10, 11, 12, 13, 14, 15, 16),
        invertdepth = newSelectBox(properties, "invertdepth", 3, 39, 5, false, true),
    }

    local sliderTb = {
       distance = newSlider(properties, "distance", 32, 512, 49, 2, 6),
       ppi = newSlider(properties, "ppi", 16, 64, 49, 2, 8),
       angle = newSlider(properties, "angle", 5, 75, 49, 2, 10),
    }

    local alarm_flag = false
    term.clear()
    term.setCursorPos(15, 8)
    term.write("Press any key to continue")
    while true do
        local eventData = { os.pullEvent() }
        local event = eventData[1]
        if event == "mouse_up" or event == "key_up" or event == "alarm"
            or event == "mouse_click" or event == "mouse_drag" or event == "key" or event == "char" then
            if not alarm_flag then
                alarm_flag = true
            else
                term.clear()
                term.setCursorPos(18, 1)
                term.write("==Settings==")
                term.setCursorPos(1, 5)
                term.write("Distance:")
                term.setCursorPos(20, 5)
                term.write("invertdepth:")
                term.setCursorPos(1, 7)
                term.write("PPI:")
                term.setCursorPos(1, 9)
                term.write("Angle:")
                term.setCursorPos(25, 5)
                term.write("Invert Depth:")
                term.setCursorPos(1, 11)
                term.write("Raycaster Number:")
                term.setCursorPos(1, 13)
                term.write("SET:")
                for k, v in pairs(fieldTb) do
                    v:paint()
                end

                for k, v in pairs(selectBoxTb) do
                    v:paint()
                end

                for k, v in pairs(sliderTb) do
                    v:paint()
                end

                term.setCursorPos(termUtil.cpX, termUtil.cpY)

                if event == "mouse_click" then
                    term.setCursorBlink(true)
                    local x, y = eventData[3], eventData[4]
                    for k, v in pairs(fieldTb) do
                        if y == v.y and x >= v.x and x <= v.x + v.len then
                            v:click(x, y)
                        end
                    end
                    for k, v in pairs(selectBoxTb) do
                        if y == v.y then
                            v:click(x, y)
                        end
                    end
                    for k, v in pairs(sliderTb) do
                        if y == v.y then
                            v:click(x, y)
                        end
                    end
                elseif event == "mouse_drag" then
                    local x, y = eventData[3], eventData[4]
                    for k, v in pairs(sliderTb) do
                        if y == v.y then
                            v:click(x, y)
                        end
                    end
                elseif event == "key" then
                    local key = eventData[2]
                    for k, v in pairs(fieldTb) do
                        if termUtil.cpY == v.y and termUtil.cpX >= v.x and termUtil.cpX <= v.x + v.len then
                            v:inputKey(key)
                            properties._needs_update = true  -- 标记需要更新
                        end
                    end
                elseif event == "char" then
                    local char = eventData[2]
                    for k, v in pairs(fieldTb) do
                        if termUtil.cpY == v.y and termUtil.cpX >= v.x and termUtil.cpX <= v.x + v.len then
                            v:inputChar(char)
                            properties._needs_update = true  -- 标记需要更新
                        end
                    end
                end
                system.updatePersistentData()
            end
        end
    end
end
parallel.waitForAll(cam, runTerm)
